跳到主要内容
Cowers://KNOWLEDGE
全部文章
Python

Python 面向对象:从类与实例到 MRO

面向 Java 背景学习者,系统梳理 Python 的类、继承、MRO、魔术方法、封装与多态。

INFO 阅读提示 本笔记面向有 Java 背景的学习者,重点标注了 Python 与 Java 的差异。 🐤 = 鸭子类型相关 ⚠️ = 容易踩的坑 🆚 = 与 Java 的区别


目录


ABSTRACT 面向对象三大特性 封装(隐藏内部细节)·继承(复用与扩展)·多态(同一接口不同行为)

一、类与实例

类是模板,实例是用模板造出来的具体对象。

class Animal:
    kingdom = "Animalia"          # 类属性:所有实例共享
 
    def __init__(self, name, age):  # 构造函数
        self.name = name          # 实例属性:每个实例独立
        self.age = age
 
    def speak(self):
        return f"我叫 {self.name}"
 
a = Animal("旺财", 3)
a.speak()          # 我叫 旺财
Animal.kingdom     # Animalia(也可通过类名访问)

类属性 vs 实例属性

类属性很像 Java 的静态变量:属于类本身、所有实例共享、可用类名访问。

WARNING ⚠️ 类属性赋值陷阱(和 Java 不一样) 通过实例给类属性赋值,不会修改类属性,而是新建一个同名实例属性把它遮住。

class Dog:
    count = 0
 
d1 = Dog()
d1.count = 99      # 新建 d1 的实例属性,不是改类属性!
print(d1.count)    # 99   ← 实例属性
print(Dog.count)   # 0    ← 类属性没变
 
Dog.count = 99     # ✅ 必须通过类名才能改类属性

NOTE 类属性 vs 实例属性,一句话区分 类属性存在类对象里(如 Animal.kingdom),所有实例查找共享;实例属性存在具体对象里(如 a.name),每个对象单独一份。

a = Animal("旺财", 3)
b = Animal("小黑", 5)
a.name      # 旺财
b.name      # 小黑 ← 互不影响,各自一份
# a.__dict__ = {"name": "旺财", "age": 3}
# b.__dict__ = {"name": "小黑", "age": 5}

这条区别在继承里还会反复用到:类属性是「类之间查找共享」,实例属性是「每个对象单独拥有」。

动态访问属性:getattr / setattr

getattr / setattr内置函数(不是类自动生成的),相当于 Java 的反射。 当属性名是运行时才知道的字符串时才需要它们。

field = "name"             # 属性名来自变量/输入/配置
getattr(dog, field)        # 等价于 dog.name
setattr(dog, field, "小黑") # 等价于 dog.name = "小黑"
getattr(dog, "color", "未知")  # 第三个参数:不存在时的默认值

TIP 和 eval 的关系 思路类似(把字符串当标识符用),但 getattreval安全子集——只能访问属性,不能执行任意代码。能用 getattr 就别用 eval


二、继承

class Dog(Animal):              # 继承 Animal(Java 的 extends)
    def __init__(self, name, age, breed):
        super().__init__(name, age)  # 调用父类构造函数
        self.breed = breed
 
    def speak(self):            # 方法重写:覆盖父类同名方法
        return f"汪!我是 {self.name}"

是否必须写 super().init()

WARNING ⚠️ 子类一旦写了 __init__ 就必须手动调 super()

情况 结果
子类没有 __init__ 自动继承父类的,无需手写
子类 __init__ 但没调 super() 父类属性全部丢失(self.name 不存在)

🆚 Java 会自动插入 super(),Python 完全靠你自己写。

NOTE 没调 super(),丢的只是「实例属性」 即使子类 __init__ 没写 super().__init__(),父类的方法、类属性、类方法、静态方法照样继承——它们都挂在上,天然可见。 唯一丢的是父类 __init__self.xxx = ... 创建的实例属性:那段代码得靠 __init__ 跑起来才生效,而子类的 __init__ 把父类的覆盖了、没机会执行。

class Animal:
    kind = "动物"              # 类属性     → 继承
    def eat(self): ...         # 方法       → 继承
    def __init__(self):
        self.name = "动物"      # 实例属性   → 丢失(除非调 super)
 
class Dog(Animal):
    def __init__(self):
        self.color = "黑色"     # 覆盖父类 __init__,且没调 super
 
d = Dog()
d.eat()        # ✅ 方法继承
d.kind         # ✅ 类属性继承 → "动物"
d.color        # ✅ "黑色"
d.name         # ❌ AttributeError:父类 __init__ 没执行

常用判断函数

isinstance(d, Dog)      # 实例是否属于某类(含父类)
issubclass(Dog, Animal) # 类是否是某类的子类

类属性的继承:查找而非复制

NOTE 子类不会「复制」父类的类属性 子类读类属性时,是沿 MRO 去父类查找,并没有在自己身上存一份。

class Animal:
    kind = "动物"
class Dog(Animal):
    pass
 
Dog.kind                   # "动物"(从父类找到的)
"kind" in Dog.__dict__     # False ← 子类自己没有
"kind" in Animal.__dict__  # True  ← 真身在父类

WARNING ⚠️ 子类赋值 = 新建覆盖,不改父类 这和[实例赋值陷阱](#类属性 vs 实例属性)是一对孪生坑,只是换成了「子类 vs 父类」:

Dog.kind = ""
Dog.kind                 # "狗"
Animal.kind              # "动物" ← 父类没变
"kind" in Dog.__dict__   # True  ← 赋值后子类才有了自己的 kind

Dog.kind = "狗" 是在 Dog新建一个 kind 把父类的遮住,不是修改 Animal.kind

DANGER ⚠️ 可变类属性会「串台」 如果类属性是 list / dict 这类可变对象,子类没赋值时拿到的是父类那同一个对象,原地修改会改到父类(以及所有兄弟子类):

class Animal:
    nums = []
class Dog(Animal):
    pass
 
Dog.nums.append(1)   # 原地修改 → 改的是父类那个列表
Animal.nums          # [1] ← 被连累了
 
Dog.nums = [1, 2, 3] # 重新赋值 → 子类新建,与父类互不影响
Animal.nums          # []   (若上面没 append 过)

规律:原地修改(append/update)会穿透到父类,重新赋值只在子类生效。

想从子类改父类的类属性,怎么办

既然 Dog.kind = x 只会创建子类自己的属性,要真正改到父类,用下面三种之一:

# 方式一:直接用父类名(最清楚,推荐)
Animal.kind = "生物"
 
# 方式二:通过 __bases__ 找到父类(能用但太绕,不推荐)
Dog.__bases__[0].kind = "生物"   # __bases__ 是直接父类的元组 (Animal,)
 
# 方式三:父类提供类方法,内部写死父类名
class Animal:
    kind = "动物"
    @classmethod
    def change_kind(cls, value):
        Animal.kind = value      # ⚠️ 必须写 Animal,不能写 cls

WARNING ⚠️ 方式三里别写 cls.kind = value 子类调用时 clsDogcls.kind = value 会变成给 Dog 新建属性,又掉进「赋值=覆盖」的陷阱。要改父类就得写死 Animal.kind


三、super() 与 MRO

NOTE super() 不是「父类对象」 它是一个代理对象,指向 MRO 链里当前类的下一个。 单继承时下一个恰好是父类,所以看起来像父类。

什么是 MRO

MRO = Method Resolution Order(方法解析顺序),即调用方法时 Python 按什么顺序去各个类里找。

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
 
print(D.__mro__)
# (D, B, C, A, object)
      A
     / \
    B   C        MRO: D → B → C → A → object
     \ /
      D

多继承:同名方法谁优先

括号里左边优先,但底层由 C3 算法保证三条规则:

  1. 子类永远排在父类前面
  2. 括号里的左右顺序保持不变
  3. 两条规则冲突时直接报 TypeError
class A:
    def hello(self): return "A"
class B(A):
    def hello(self): return "B"
class C(A):
    def hello(self): return "C"
class D(B, C): pass
 
D().hello()   # "B",因为 MRO 里 B 在 C 前面

EXAMPLE super() 沿 MRO 逐级调用

class D(B, C):           # MRO: D → B → C → A
    def __init__(self):
        super().__init__()  # B 里的 super() 调的是 C,不是 A!

多继承时各类的 super() 串成一条链,每个类只执行一次。


四、魔术方法(Magic Methods)

也叫 dunder methods(double underscore,双下划线)。

NOTE 为什么叫「魔术」不需要显式调用它们,Python 在特定时机自动触发。比如写 + 自动调 __add__print(obj) 自动调 __str__。 双下划线命名是 Python 官方保留的,不要自己发明新的 __xxx__

不定义会怎样

类型 不定义的结果
__str__ __eq__ __repr__ object 的默认实现,不报错
__add__ __len__ __getitem__ 直接报 TypeError
ABC 里的 @abstractmethod 实例化时就报错

判断标准:object 基类里有没有默认实现

分类速查

生命周期

方法 触发时机 说明
__new__ 创建实例 __init__ 更早,单例/继承不可变类型时才用
__init__ 创建实例 初始化属性,最常用
__del__ 对象被回收 析构,时机不保证,慎用

字符串表示

方法 触发方式 受众
__str__ print() / str() 给用户看
__repr__ repr() / 终端输入变量名 给开发者看
__format__ f"{obj:spec}" 格式化语法

比较运算

方法 运算符
__eq__ __ne__ == !=
__lt__ __le__ < <=
__gt__ __ge__ > >=

TIP @functools.total_ordering 只需定义 __eq__ 和一个比较方法,其余四个自动推导。

数学运算

方法 运算符 反向版本
__add__ + __radd__
__sub__ - __rsub__
__mul__ * __rmul__
__truediv__ / __rtruediv__

EXAMPLE 反向方法(r 前缀)什么时候触发 左操作数不支持该运算时,Python 转去调右操作数的反向方法。

v * 3   # 调 v.__mul__(3)
3 * v   # int 不认识 Vector → 转调 v.__rmul__(3)

日常很少用,主要在写自定义数值类型(向量、矩阵、分数)或和 numpy 混用时才需要。

容器协议(让类像 list / dict)

方法 触发方式
__len__ len(obj)
__getitem__ obj[key]
__setitem__ obj[key] = v
__contains__ x in obj
__iter__ / __next__ for x in obj

上下文管理(with 语句)

方法 触发时机 说明
__enter__ 进入 with 块 返回值绑定到 as 变量
__exit__ 离开 with 块 返回 True 可吞掉异常

其他实用

方法 触发方式 说明
__call__ obj() 让实例像函数一样调用
__hash__ hash(obj) / 作字典 key ⚠️ 定义 __eq__ 后默认变 None
__bool__ if obj 没有时用 __len__ 判断

DANGER ⚠️ eqhash 联动 定义了 __eq__ 之后,__hash__ 会自动变成 None,该类实例就不能放进 set 或当 dict 的 key。需要手动补回 __hash__


五、封装

命名约定(约定而非强制)

Python 没有 private 关键字,靠命名区分访问级别:

class BankAccount:
    def __init__(self):
        self.owner = "Alice"    # 公开
        self._balance = 1000    # 保护(约定别外部访问)
        self.__secret = "key"   # 私有(名称改写为 _BankAccount__secret)

🆚 Java 有真正的 private,Python 的 __ 只是「名称改写」,仍能强行访问,只是不推荐。

@property 装饰器

用属性语法访问,同时加入校验逻辑(Java 的 getter/setter,但更干净)。

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius
 
    @property                    # getter
    def celsius(self):
        return self._celsius
 
    @celsius.setter              # setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("低于绝对零度")
        self._celsius = value
 
t = Temperature(25)
t.celsius = 30      # 调用 setter
t.celsius           # 调用 getter

NOTE 多个属性 & 完整生命周期 每个属性单独写一套,@xxx.setter 里的 xxx 必须和 @property 的方法名一致。

@property       → 读   p.name
@name.setter    → 写   p.name = "x"
@name.deleter   → 删   del p.name

六、多态

🐤 鸭子类型

QUOTE 「如果它走路像鸭子、叫声像鸭子,那它就是鸭子。」

Python 不看类型,只看对象有没有这个方法

def make_sound(obj):       # 没有任何类型限制
    obj.speak()            # 只要有 speak() 就能跑
 
make_sound(Dog())          # 汪!
make_sound(Cat())          # 喵~
make_sound(Robot())        # beep boop(Robot 不是 Animal,照样能用)

🆚 Java 必须声明 void makeSound(Animal animal),Python 完全不需要。

方法重写(override)

子类定义同名方法,覆盖父类的。Python 查找顺序:先找自己,再找父类

class Animal:
    def speak(self): return "..."
class Dog(Animal):
    def speak(self): return "汪!"   # 覆盖父类
 
Dog().speak()   # "汪!"

🆚 Python 没有方法重载(overload)

同名方法后面的会直接覆盖前面的,不能靠参数区分。

class Dog:
    def speak(self): return ""
    def speak(self, msg): return msg   # ⚠️ 覆盖了上面那个

用三种方式替代重载:

# 1. 默认参数(最常用)
def speak(self, msg="", times=1):
    return msg * times
 
# 2. *args / **kwargs 接收任意参数
def speak(self, *args): ...
 
# 3. 类型判断(少用)
def speak(self, msg=None):
    if isinstance(msg, int): ...

SUMMARY 三个概念的关系

  • 方法重写:怎么定义不同的行为
  • 鸭子类型:怎么使用这些不同的行为
  • 方法重载:Python 没有,用默认参数替代

七、抽象基类与接口

🆚 Python 没有 interface 关键字,用**抽象基类(ABC)**替代。

from abc import ABC, abstractmethod
 
class Shape(ABC):                # 相当于 interface
    @abstractmethod
    def area(self): ...          # 子类必须实现
 
    def describe(self):          # 普通方法,子类直接继承
        return f"面积是 {self.area()}"
 
class Circle(Shape):
    def __init__(self, r): self.r = r
    def area(self): return 3.14 * self.r ** 2
 
# Shape()  ← 报错:不能实例化含抽象方法的类

NOTE 比 Java interface 更灵活 Java 的 interface 纯抽象,Python 的 ABC 可以混入普通方法——其实更像 Java 的抽象类。Python 把抽象类和接口合二为一了。

如果只是想调用某方法,靠鸭子类型即可,根本不需要 ABC。 想强制子类实现某些方法,才用 ABC。

abc 是标准库

abc 是 Python 标准库,装好 Python 就有,import abc 直接用,无需 pip。

类别 例子 是否需要安装
内置 print len isinstance 连 import 都不用
标准库 abc os math collections functools import 即可,自带
第三方 numpy pandas requests 需要 pip install

八、方法的三种类型

类里能定义三种方法,区别在于第一个参数能访问什么

class Demo:
    data = "类属性"
 
    def instance_method(self):     # 普通方法:参数 self(实例)
        return self.x, Demo.data   # 能访问实例 + 类
 
    @classmethod
    def class_method(cls):         # 类方法:参数 cls(类)
        return cls.data            # 只能访问类
 
    @staticmethod
    def static_method(a, b):       # 静态方法:无 self / cls
        return a + b               # 谁都访问不了,纯计算
装饰器 第一个参数 能访问 典型用途
普通方法 self(实例) 实例属性 + 类属性 操作具体对象
类方法 @classmethod cls(类) 仅类属性 工厂方法
静态方法 @staticmethod 都不能 工具/校验/转换函数

TIP 怎么选——一句话决策

  • 用到了 self(实例数据)→ 普通方法
  • 用到了 cls(类属性、或要造对象)→ 类方法
  • 两个都没用到 → 静态方法(甚至可考虑写成模块级普通函数)

WARNING ⚠️ 用类名调实例方法,self 要自己传 实例方法本质是「类里的普通函数」,只有用对象调时 Python 才自动把对象塞给 self

class Student:
    def say(self): print("hello")
 
s = Student()
s.say()            # ✅ 等价于 Student.say(s),自动传 s
Student.say(s)     # ✅ 手动传对象,也行
Student.say()      # ❌ TypeError: missing 1 required positional argument: 'self'

带参数时第一个位置依然是 self:Student.introduce(s, "张三") 才对;写 Student.introduce("张三") 会把 "张三" 当成 self,然后报 name 缺参。 类方法 / 静态方法没这问题——cls 由 Python 自动给,静态方法压根不需要。

类方法 @classmethod

第一个参数 cls 指向类本身,不是实例。可用类名直接调,不需要创建对象。

class Dog:
    count = 0
 
    @classmethod
    def class_method(cls):
        return cls.count       # 访问类属性
 
Dog.class_method()             # ✅ 直接用类名调用

工厂方法(类方法最常见的用途)

NOTE 什么是工厂方法 一个专门用来「生产」对象的方法——你不直接构造,而是调一个方法帮你造。 因为 Python 只能有一个 __init__(没有重载),工厂方法用来补足「多种创建方式」。

class Date:
    def __init__(self, y, m, d):     # 唯一的构造函数
        self.y, self.m, self.d = y, m, d
 
    @classmethod
    def from_string(cls, s):         # 工厂①:从字符串造
        y, m, d = map(int, s.split("-"))
        return cls(y, m, d)          # cls(...) 等价于 Date(...)
 
    @classmethod
    def today(cls):                  # 工厂②:造今天
        import datetime
        t = datetime.date.today()
        return cls(t.year, t.month, t.day)
 
# 多个「构造入口」
d1 = Date(2025, 6, 14)               # 常规构造
d2 = Date.from_string("2025-06-14")  # 工厂方法
d3 = Date.today()                    # 工厂方法

WARNING ⚠️ 工厂里要用 cls(...) 而不是写死 Date(...) cls 会自动指向实际调用的类,继承时才不出错。

class ChineseDate(Date): pass
d = ChineseDate.from_string("2025-06-14")
type(d)   # ChineseDate ✅(写死 Date(...) 则会返回 Date ❌)

EXAMPLE 标准库里早就用过的工厂方法

dict.fromkeys(["a", "b"], 0)   # dict 的工厂方法
int.from_bytes(b"\x00\x10")    # int 的工厂方法
datetime.now()                 # datetime 的工厂方法

命名习惯:from_xxx(从某种数据造)或 create_xxx

静态方法 @staticmethod

没有 self 也没有 cls,本质就是「恰好放在类里的普通函数」。 用途:逻辑上属于这个类、但不依赖任何实例或类状态的工具函数。

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
 
    @staticmethod
    def c_to_f(c):                 # 纯转换,不需要任何实例数据
        return c * 9 / 5 + 32
 
    @staticmethod
    def is_freezing(celsius):      # 纯判断
        return celsius <= 0
 
Temperature.c_to_f(25)            # 77.0,不用创建对象就能调

TIP 静态方法的意义在于「归类」 把相关逻辑收进类里,比散落在外面更清晰。但如果一个静态方法和这个类毫无关系,那它更适合直接放在模块里当普通函数。


九、Python vs Java 速查表

概念 Java Python
静态变量 static 字段 类属性(⚠️ 实例赋值有陷阱)
私有成员 private(强制) _ / __(约定)
getter/setter 手写方法 @property
调用父类构造 自动插入 super() 手动写 super().__init__()
单继承 / 多继承 仅单继承 支持多继承(注意 MRO)
方法重写 ✅ override ✅ override
方法重载 ✅ overload ❌ 用默认参数替代
多态 需声明类型 🐤 鸭子类型,不看类型
接口 interface 抽象基类 ABC
反射 Reflection API getattr / setattr
多种构造函数 构造函数重载 @classmethod 工厂方法
静态方法 static 方法 @staticmethod(无 self/cls)

SUCCESS 复习重点

  1. 类属性赋值陷阱:实例赋值会新建实例属性遮住类属性,必须用类名修改
  2. 子类有 __init__ 必须手动调 super();没调时只丢父类实例属性,方法/类属性/类方法/静态方法照常继承
  3. super() 沿 MRO 走,不是简单的父类
  4. __eq____hash__ 的联动
  5. Python 没有重载,用默认参数
  6. 鸭子类型 + ABC 替代 Java 的接口
  7. 三种方法的区别:self(实例) / cls(类) / 无(静态);用类名调实例方法要手动传 self
  8. 工厂方法用 @classmethod + cls(...),弥补单一 __init__
  9. 类属性继承是查找而非复制:可变类属性原地改会串到父类;想改父类类属性用 父类名.属性,类方法里别写 cls.属性